﻿
namespace FolderSync
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.IO;
    using System.Security;
    using System.Threading;
    using System.Diagnostics;

    public class Syncer
    {
        /// <summary>
        /// When true the Syncer will only report changes on the watched folder(s).
        /// </summary>
        public INotifier Notifier { get; set; }


        public int MaxErrorCount = 100;
        /// <summary>
        /// 
        /// </summary>
        private SortedList<int, FileEvent> _eventQueue = new SortedList<int, FileEvent>();
        private bool _threadRunning = true;
        private int _eventSeq;
        /// <summary>
        /// Count of events on the processing queue
        /// </summary>
        public int Count { get { return _eventQueue.Count; } }
        private float _progress = 0;

        public delegate void StatusChangedHandler(Syncer sender, string status);
        public event StatusChangedHandler StatusChanged;
        private string _status = "Syncer is idle.";
        public string Status
        {
            get
            {
                return string.Format("{0}. {1}%. {2} events in queue.", _status, (int)(_progress * 100), Count);
            }
            private set { 
                _status = value;
                StatusChanged(this, Status);
            }
        }
        private bool NotifyProgress(float progress)
        {
            _progress = progress;
            StatusChanged(this, Status);
            return true;
        }
        public SyncStats Statistics = new SyncStats();

        private enum EventProcessResults
        {
            Success,
            Error,
            FileLocked,
            Warning
        }

        public void Stop()
        {
            _threadRunning = false;
        }

        public void Run()
        {
            var notifiedEmpty = false;
            try
            {
                while (_threadRunning)
                {
                    var doneAny = false;
                    while (_threadRunning && Dequeue())
                    {
                        notifiedEmpty = false; doneAny = true;
                    }
                    if (!doneAny && !notifiedEmpty)
                    {
                        notifiedEmpty = true;
                        Notifier.Message("RUN: Message queue is empty or no event reached expiry. Waiting.");
                    }
                    Thread.Sleep(50);
                }
            }
            catch (Exception e)
            {
                Notifier.Error(e.Message);
                Notifier.Debug(e.StackTrace);
            }
            Notifier.Message("RUN: Syncer stopped.");
        }
        public bool Dequeue()
        {
            return ProcessEvent(DequeueEvent());
        }
        public FileEvent DequeueEvent()
        {
            lock (_eventQueue)
            {
                try
                {
                    var evt = _eventQueue.Where(w => w.Value.Expiry <= DateTime.Now).First().Value;
                    _eventQueue.Remove(evt.key);
                    return evt;
                }
                catch { return null; }
            }
        }
        public bool ProcessEvent(FileEvent evt)
        {
            if (evt == null) return false;
            try
            {
                var eventProcessResult = OnChangedImpl(evt);
                switch (eventProcessResult)
                {
                    case EventProcessResults.Success:
                    case EventProcessResults.Warning:
                    case EventProcessResults.Error:
                        break;
                    case EventProcessResults.FileLocked:
                        // failed: keep for later: but need check the queue, as a new event for the same file may 
                        // have been inserted between dequeue and now.
                        Notifier.Debug("DEQ: OnChangedImpl failed for {0}:{1}.", evt, eventProcessResult);
                        lock (_eventQueue)
                        {
                            var newerEvents = _eventQueue
                                .Where(w =>
                                    (
                                        (w.Value.ChangeType == CompoundWatcherChangeTypes.ChNamed
                                        || w.Value.ChangeType == CompoundWatcherChangeTypes.Renamed
                                        )
                                        && w.Value.OldFullPath == evt.FullPath
                                    ) || (
                                        (w.Value.ChangeType == CompoundWatcherChangeTypes.Changed
                                        || w.Value.ChangeType == CompoundWatcherChangeTypes.Created
                                        || w.Value.ChangeType == CompoundWatcherChangeTypes.Deleted
                                        )
                                        && w.Value.FullPath == evt.FullPath
                                    )
                                ).ToArray();
                            Notifier.Debug("DEQ: OnChangedImpl failed for {0}. New events in queue:{1}.", evt, newerEvents.Count());
                            if (newerEvents.Count() == 0)
                            {
                                if (MaxErrorCount == 0 || evt.ErrorCount < MaxErrorCount)
                                { // allow to forget about an event after that many retries
                                    // TODO: special case with create failing after a delete has occured... 
                                    // if requeued we enter an infinite loop.
                                    // typically the delete happened between the dequeue of the create and processing of the create event.
                                    evt.key = ++_eventSeq;
                                    _eventQueue.Add(evt.key, evt);
                                    Notifier.Warning("DEQ: Event in error, requeued:{0}", evt);
                                }
                                else Notifier.Error("DEQ: Event exceeds MaxErrorCount, discarded:{0}", evt);
                            }
                            else
                            {
                                // remove from the queue all events posted after dequeue 
                                foreach (var k in newerEvents.Select(w => w.Key)) _eventQueue.Remove(k);
                                // reinsert the event that failed
                                evt.key = ++_eventSeq;
                                _eventQueue.Add(evt.key, evt);
                                // reinsert the newer events with the OnChanged method to apply the queue logic
                                foreach (var qevt in newerEvents.OrderBy(w => w.Key).Select(w => w.Value))
                                    this.Enqueue(new FileEvent(qevt.TranslatePath, qevt.OriginalEvent), qevt.ErrorCount);
                                Notifier.Warning("Event in error, merged with {1} element(s) of the queue:{0}", evt, newerEvents.Count());
                                List(onDebugOnly: true);
                            }
                        }
                        break;
                    default:
                        break;
                }
                return true;
            }
            catch (Exception e)
            {
                Notifier.Error(e.Message);
                Notifier.Debug(e.StackTrace); return false;
            }
        }
        public void List(bool onDebugOnly = true)
        {

            lock (_eventQueue)
            {
                var messageLevel = onDebugOnly ? NotifierLevel.Debug : NotifierLevel.Message;
                Notifier.Message(messageLevel, "List: {0} elements in queue",
                    _eventQueue.Count(e => { Notifier.Message(messageLevel, "List: " + e.ToString()); return true; })
                );
            }
        }
        // Define the response to the event handlers.
        public void Enqueue(FileEvent rawEvent, int ErrorCount)
        {
            lock (_eventQueue)
            {
                //Notifier.Message("Event queue locked:{0}", evt));
                // remove all previously queued events which denote a creation or change of the same object. EnqueueRuler cares for that.
                // 1st, search for a file event for same file in the queue
                FileEvent evt = null;
                var formerEvents = _eventQueue.Where(
                            qevt =>
                                ((rawEvent.ChangeType == CompoundWatcherChangeTypes.Renamed
                                 || rawEvent.ChangeType == CompoundWatcherChangeTypes.ChNamed
                                  ? rawEvent.OldFullPath : rawEvent.FullPath) == qevt.Value.FullPath
                                )
                                && qevt.Value.EventTime < rawEvent.EventTime)
                                .OrderBy(qevt => qevt.Value.EventTime);
                formerEvents.All(qevt =>
                {
                    _eventQueue.Remove(qevt.Key);
                    evt += qevt.Value;
                    return true;
                });
                evt += rawEvent;
                evt.key = ++_eventSeq;
                try
                {
                    _eventQueue.Add(evt.key, evt);
                }
                catch (Exception ex)
                {
                    Notifier.Error("Error on processing raw event {0}\r\nAgged event {1}\r\n{2}", rawEvent, evt, ex.ToString());
                    Notifier.Debug(ex.StackTrace);
                }
                List(onDebugOnly: true);
            }
            //Notifier.Message("Event queue unlocked: {0}", evt));
        }

        private EventProcessResults OnChangedImpl(FileEvent fe)
        {
            // Specify what is done when a file is changed, created, or deleted.
            SyncStats stats = new SyncStats();
            Notifier.Message("DEQ: Dequeuing " + fe);
            Status = string.Format("Processing event {0}", fe);
            //return true;

            var sourceFile = new FileInfo(fe.FullPath);
            var sourceOldFile = string.IsNullOrEmpty(fe.OldFullPath) ? null : new FileInfo(fe.OldFullPath);
            var targetFile = new FileInfo(fe.TranslatePath(sourceFile.FullName));
            var targetOldFile = sourceOldFile == null ? null : new FileInfo(fe.TranslatePath(sourceOldFile.FullName));

            var sourceDirectory = new DirectoryInfo(fe.FullPath);
            var sourceOldDirectory = string.IsNullOrEmpty(fe.OldFullPath) ? null : new DirectoryInfo(fe.OldFullPath);
            var targetDirectory = new DirectoryInfo(fe.TranslatePath(sourceDirectory.FullName));
            var targetOldDirectory = sourceOldFile == null ? null : new FileInfo(fe.TranslatePath(sourceOldFile.FullName));

            //if (!sourceFile.Exists && fe.ChangeType != CompoundWatcherChangeTypes.Deleted)
            //{
            //    Notifier.Warning("DEQ: File does not exist: {0} {1}", fe.FullPath, fe.ChangeType));
            //}
            //else
            //{
            try
            {
                switch (fe.ChangeType)
                {
                    case CompoundWatcherChangeTypes.Created:
                        if (sourceFile.Exists)
                        {
                            if (targetDirectory.Exists)
                            {
                                Notifier.Error("DEQ: File formerly exist as directory on target. {0}", fe);
                                return EventProcessResults.Error;
                            }
                            else
                            {
                                stats.CountCopiedFile(sourceFile);
                                targetFile.Directory.EnsureExists(true);
                                sourceFile.NotifyCopyTo(targetFile.FullName, NotifyProgress);
                                Notifier.Message("DEQ: Created (copied) file {1} to {0}.", targetFile.FullName, sourceFile.FullName);
                            }
                        }
                        else if (sourceDirectory.Exists)
                        {
                            if (targetFile.Exists)
                            {
                                Notifier.Error("DEQ: {0}: Directory formerly exist as file on target: {1}", fe);
                                return EventProcessResults.Error;
                            }
                            else
                            {
                                stats.CountFolder(sourceDirectory);
                                targetDirectory.Create();
                                Notifier.Message("DEQ: Created directory {0} from {1}.", targetDirectory.FullName, sourceDirectory.FullName);
                            }
                        }
                        else
                        {
                            Notifier.Warning("DEQ: {0}: File or directory does not exist on source.", fe);
                            return EventProcessResults.Warning;
                        }
                        break;
                    case CompoundWatcherChangeTypes.Changed:
                        if (sourceFile.Exists)
                        {
                            if (targetDirectory.Exists)
                            {
                                Notifier.Error("DEQ: {0}: File formerly exists as directory on target.", fe);
                                return EventProcessResults.Error;
                            }
                            else
                            {
                                targetFile.Directory.EnsureExists(true);
                                stats.CountCopiedFile(sourceFile);
                                sourceFile.NotifyCopyTo(targetFile.FullName, NotifyProgress);
                                Notifier.Message("DEQ: Copied file {1} to {0}.", targetFile.FullName, sourceFile.FullName);
                            }
                        }
                        else if (sourceDirectory.Exists)
                        {
                            if (targetFile.Exists)
                            {
                                Notifier.Error("DEQ: {0}: Directory formerly exists as file on target.", fe);
                                return EventProcessResults.Error;
                            }
                            else
                            {
                                Notifier.Message("DEQ: {0} Directory change ignored.", fe);
                            }
                        }
                        else
                        {
                            Notifier.Warning("DEQ: {0}: File or directory does not exist on source.", fe);
                            return EventProcessResults.Warning;
                        }
                        break;
                    case CompoundWatcherChangeTypes.Renamed:
                        Debug.Assert(targetOldDirectory != null && targetOldFile != null);
                        if (string.IsNullOrEmpty(fe.OldFullPath))
                        {
                            Notifier.Error("DEQ: Renamed event without old reference:{0}", fe);
                        }
                        else if (targetOldFile.Exists)
                        {
                            if (targetFile.Exists || targetDirectory.Exists)
                            {
                                Notifier.Error("DEQ: {0}: File or folder formerly exists on target.", fe);
                                return EventProcessResults.Error;
                            }
                            else
                            {
                                stats.CountMovedFile(sourceFile);
                                targetOldFile.MoveTo(targetFile.FullName);
                                Notifier.Message("DEQ: Renamed file {1} to {0}.", targetOldFile.FullName, targetFile.FullName);
                            }
                        }
                        else if (targetOldDirectory.Exists)
                        {
                            if (targetFile.Exists || targetDirectory.Exists)
                                Notifier.Warning("DEQ: {0}: File or folder formerly exists on target.", fe);
                            else
                            {
                                stats.CountFolder(sourceDirectory);
                                targetOldDirectory.MoveTo(targetDirectory.FullName);
                                Notifier.Message("DEQ: Renamed directory {1} to {0}.", targetOldDirectory.FullName, targetDirectory.FullName);
                            }
                        }
                        else
                        {
                            Notifier.Warning("DEQ: {0}: File or folder does not exist on target.", fe);
                            if (sourceFile.Exists)
                            {
                                targetFile.Directory.EnsureExists(true);
                                stats.CountCopiedFile(sourceFile);
                                sourceFile.NotifyCopyTo(targetFile.FullName, NotifyProgress);
                                Notifier.Message("DEQ: Copied file {1} to {0}.", targetFile.FullName, sourceFile.FullName);
                            }
                            else if (sourceDirectory.Exists)
                            {
                                Notifier.Error("DEQ: Cannot rename a directory that does not exist on target. Use /mir to synchronize: {0}", sourceDirectory.FullName);
                            }
                            return EventProcessResults.Success;
                        }
                        break;
                    case CompoundWatcherChangeTypes.ChNamed:
                        Debug.Assert(targetOldDirectory != null && targetOldFile != null);
                        if (string.IsNullOrEmpty(fe.OldFullPath))
                        {
                            Notifier.Error("DEQ: ChNamed event without old reference:{0}", fe);
                        }
                        else if (sourceFile.Exists)
                        {
                            if (targetFile.Exists || targetDirectory.Exists)
                            {
                                Notifier.Error("DEQ: {0}: File or folder formerly exists on target.", fe);
                                return EventProcessResults.Error;
                            }
                            else
                            {
                                if (targetOldFile.Exists)
                                {
                                    targetOldFile.Delete();
                                    Notifier.Message("DEQ: Old file {0} deleted.", targetOldFile.FullName);
                                }
                                targetFile.Directory.EnsureExists(true);
                                stats.CountCopiedFile(sourceFile);
                                sourceFile.NotifyCopyTo(targetFile.FullName, NotifyProgress);
                                Notifier.Message("DEQ: Copied file {1} to {0}.", sourceOldFile.FullName, targetFile.FullName);
                            }
                        }
                        else if (sourceDirectory.Exists)
                        { // treate as a simple directory rename
                            if (targetFile.Exists || targetDirectory.Exists)
                            {
                                Notifier.Warning("DEQ: File or folder formerly exists on target. {0}", fe);
                                return EventProcessResults.Error;
                            }
                            else if (targetOldDirectory.Exists)
                            {
                                {
                                    stats.CountFolder(sourceDirectory);
                                    targetOldDirectory.MoveTo(targetDirectory.FullName);
                                    Notifier.Message("DEQ: Renamed directory {1} to {0}.", targetOldDirectory.FullName, targetDirectory.FullName);
                                }
                            }
                        }
                        else
                        {
                            Notifier.Warning("DEQ: {0}: File or folder does not exist on source.", fe);
                            return EventProcessResults.Warning;
                        }
                        break;

                    case CompoundWatcherChangeTypes.Deleted:
                        if (targetFile.Exists)
                        {
                            stats.CountDeletedFile(targetFile);
                            targetFile.Delete();
                            Notifier.Message("DEQ: Deleted file {0}", targetFile.FullName);
                        }
                        else if (targetDirectory.Exists)
                        {
                            stats.CountDeletedFile(targetDirectory);
                            targetDirectory.Delete(true);
                            Notifier.Message("DEQ: Deleted directory {0}", targetDirectory.FullName);
                        }

                        else
                        {
                            Notifier.Warning("DEQ: {0}: File or folder does not exist on target.", fe);
                        }
                        break;
                    default:
                        break;
                }
                stats.Done();
                Statistics += stats;
                return EventProcessResults.Success;
            }
            catch (Exception e)
            {
                if (e is System.UnauthorizedAccessException ||
                    e is System.Security.SecurityException)
                {
                    Notifier.Warning("DEQ Exception: {0} {1}, {2}\r\n{3}", e.GetType().Name, e.Message, fe, e.StackTrace);
                    return EventProcessResults.Error;
                }
                else if (e is System.IO.IOException)
                {
                    Notifier.Warning("DEQ Exception: {0} {1}, {2}\r\n{3}", e.GetType().Name, e.Message, fe, e.StackTrace);
                    fe.Expiry = DateTime.Now + new TimeSpan(hours: 0, minutes: 0, seconds: 5);
                    return EventProcessResults.FileLocked;
                }
                else
                {
                    Notifier.Error("DEQ Exception: {0} {1}, {2}\r\n{3}", e.GetType().Name, e.Message, fe, e.StackTrace); 
                    return EventProcessResults.Error;
                }
            }
            finally
            {
                Status = "Syncer is idle.";
            }
        }
    }
}

